package org.pz.controller;

import lombok.extern.slf4j.Slf4j;
import org.pz.constants.RedisConstant;
import org.pz.entity.MallResult;
import org.pz.entity.UserEntity;
import org.pz.mapper.UserMapper;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 用来模拟 redis 缓存使用
 * */
@RestController
@RequestMapping("/redis/demo")
@Slf4j
public class RedisDemoController {

    @Autowired
    private UserMapper userMapper;

    private RedissonClient redissonClient;

    private RedisTemplate redisTemplate;

    public RedisDemoController(RedissonClient redissonClient,RedisTemplate redisTemplate){
        this.redissonClient = redissonClient;
        this.redisTemplate = redisTemplate;
    }

    private final static String findUserKeySuffix = "findUserKey_username";
    private final static String redisLockKey = "redisLockKey_";
    private final static String findUserRWKey = "findUserRWKey_";

    private RBloomFilter bloomFilter;

    @PostConstruct
    public void init(){
        //初始化布隆过滤器，不能 flush db
        bloomFilter = redissonClient.getBloomFilter("test_bloom_filter");
        bloomFilter.tryInit(10_0000L, 0.01); // 预期数据量 10 万，误判率 1%
        //预加载数据
        List<String> list = userMapper.findAllUsername();
        list.forEach(e -> bloomFilter.add(e));
    }

    /**
     * springboot 集成的 cacheable 使用
     * 使用 @Cacheable 注解，方法返回结果会被缓存
     * 需要注意数据更改，要想办法刷新缓存，比如使用 @CacheEvict
     */
    @RequestMapping("/cacheable/{username}")
    @Cacheable(value = RedisConstant.CACHE_KEY_USER, key = "#p0")
    public MallResult cacheable(@PathVariable String username) {
        return MallResult.success( userMapper.findUserByUserName(username) );
    }

    /**
     * springboot 集成的 redis
     * 使用  @CacheEvict 来清除缓存，这里的 key 要与查询的保持一致
     * */
    @RequestMapping("/cacheEvent")
    @CacheEvict(value = RedisConstant.CACHE_KEY_USER, key = "#p0.username")
    public MallResult cacheEvent(@RequestBody UserEntity user) {
        userMapper.updateUserByUsername(user);
        return MallResult.success( );
    }

    /**
     * 缓存穿透，二选一
     *  - 布隆过滤器
     *  - null 值也存储
     * 缓存击穿
     *  - 设置数据逻辑过期时间，就是拿出来判断是否已经过期
     *  - 使用互斥锁。如果缓存失效的情况，只有拿到锁才可以查询数据库，降低了在同一时刻打在数据库上的请求，防止数据库打死
     * */
    @RequestMapping("/question/{username}")
    public MallResult question(@PathVariable String username) {
        /*
         * 1. 缓存击穿防护一，布隆过滤器
         * fixme 这里有个问题，如果我在 redis 服务端手动 flush db 把这个布隆过滤器删了，这个接口就会阻塞在这里。
         *      此时重启一下 springboot 服务，让布隆过滤器重新初始化一下就好
         * */
        if(!bloomFilter.contains(username)) {
            log.info("布隆过滤器未包含！");
            return MallResult.error("未查询到数据");
        }

        /*
         * 2. 查缓存
         */
        String redisKey = findUserKeySuffix + username;
        MallResult cacheResult;
        if( (cacheResult = findUserEntityFromCache(redisKey)) != null ){
            return cacheResult;
        }

        /*
         * 3. 查询数据库
         *   重构缓存的时候，这里添加分布式锁，防止多个线程查询数据
         *   添加分布式锁，对付缓存穿透，缓存击穿都可以
         */
        RLock lock = redissonClient.getLock(redisLockKey + redisKey);
        //这里也可以 tryLock()，返回个服务器繁忙啥的
        lock.lock();
        try {

            //这里再查一次缓存，因为除了第一个线程，其他线程获取到锁的时候，缓存应该已经好了，不需要再查数据库
            if( (cacheResult = findUserEntityFromCache(redisKey)) != null ){
                return cacheResult;
            }

            //真正查询数据库
            UserEntity ue = userMapper.findUserByUserName(username);
            if (ue == null) {
                log.info("查询数据库未查到数据");
                /**
                 * 缓存击穿防护二：空值也缓存，设置哥短点的缓存时间，5s
                 */
                ue = new UserEntity.NullUserEntity();
                redisTemplate.opsForValue().set(redisKey, ue, 5, TimeUnit.SECONDS);
                return MallResult.error("未查询到数据");
            }

            //4. 缓存真正的对象
            log.info("查询数据库查到真正的数据");
            redisTemplate.opsForValue().set(redisKey, ue);
            return MallResult.success(ue);
        }finally {
            if(lock.isHeldByCurrentThread()){
                lock.unlock();
            }
        }
    }

    /*
     * 查询缓存
     */
    private MallResult findUserEntityFromCache(String redisKey){

        //这里添加读锁，为了配合 questionDoubleWriteLock 写锁的效果，不用分布式锁可不用
        RLock rwLock = redissonClient.getReadWriteLock(findUserRWKey + redisKey).readLock();
        rwLock.lock();
        try {
            UserEntity ue = (UserEntity) redisTemplate.opsForValue().get(redisKey);
            //从缓存中查到数据
            if (ue != null) {
                //这种情况是之前查到的 空对象
                if (ue instanceof UserEntity.NullUserEntity) {
                    log.info("缓存中有空对象！！");
                    return MallResult.error("未查询到数据");
                }
                //这是真正缓存的数据
                log.info("缓存中有真实对象");
                return MallResult.success(ue);
            }

            //这是缓存中没有查询到数据
            return null;
        }finally {
            if(rwLock.isHeldByCurrentThread()){}
            rwLock.unlock();
        }
    }

    /**
     * 修改数据
     * 2. 双写一致性（redis，mysql）- 分布式锁 - 强一致性
     * */
    @RequestMapping("/question/double/write/lock")
    public MallResult questionDoubleWriteLock(@RequestBody UserEntity user){
        String redisKey = findUserKeySuffix + user.getUsername();

        //这里使用读写锁
        RLock rwLock = redissonClient.getReadWriteLock(findUserRWKey + redisKey).writeLock();
        rwLock.lock();
        try{
            //模拟业务耗时
            Thread.sleep(1_000);

            //删除缓存
            redisTemplate.delete(redisKey);

            //模拟业务耗时
            Thread.sleep(1_000);

            //删除数据，重建缓存的过程 写锁
            userMapper.updateUserByUsername(user);
            return MallResult.success();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            if(rwLock.isHeldByCurrentThread()){
                rwLock.unlock();
            }
        }
    }

    /**
     * 1. 双写一致性（redis，mysql）- 延时双删 - 最终一致性
     * */
    @RequestMapping("/question/double/write/delay")
    public MallResult questionDoubleWrite(@RequestBody UserEntity user) throws InterruptedException {
        String redisKey = findUserKeySuffix + user.getUsername();

        //第一次删除缓存
        redisTemplate.delete(redisKey);

        //模拟业务耗时
        Thread.sleep(1_000);

        //操作数据（增删改都适用）
        userMapper.updateUserByUsername(user);

        /*
         * fixme 延时，为了让从库同步主库的数据，这个延时时间不好控制
         *       真实场景下，这里应该使用 mq 异步通知，然后消费者执行第二次删除
         */
        Thread.sleep(2_000);

        //第二次删除缓存，保证后续缓存是基于数据库的
        redisTemplate.delete(redisKey);
        return MallResult.success();
    }
}
